/* 
 *  Copyright 2012 CodeMagi, Inc.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.codemagi.servlets.content;

import com.codemagi.login.AccessException;
import com.codemagi.login.LoginException;
import com.codemagi.login.model.*;
import com.codemagi.servlets.AppException;
import com.codemagi.servlets.Lists;
import com.codemagi.servlets.ServletUtils;
import com.codemagi.servlets.content.model.*;
import com.codemagi.servlets.validation.*;
import com.codemagi.util.*;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.log4j.*;
import org.exolab.castor.jdo.CacheManager;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.QueryResults;

/**
 * This class is the primary controller for page editing and page serving functions. 
 * 
 * It also acts as a filter to direct vanity URLs to the proper pages
 *
 * @version 1.0
 * @author August Detlefsen for CodeMagi, Inc.
 */
public class PageController extends com.codemagi.servlets.NavigationController implements Filter {

    //set to true to view debug output
    Logger log = Logger.getLogger(this.getClass());

    //members
    protected Lists lists;
    public static final Integer ROOT_PAGE_ID = new Integer(0);

    //VIEWS specific to this application
    protected static String TEMPLATE                  = "/template.jsp";
    protected static String VIEW_EDIT_PAGE            = "/admin/form_page.jsp";
    protected static String VIEW_DELETE_PAGE          = "/admin/delete_page.jsp";
    protected static String VIEW_LIST_PAGES           = "/admin/pages.jsp";
    protected static String VIEW_EDIT_SECTION         = "/admin/form_section.jsp";
    protected static String VIEW_SET_TEAM             = "/admin/form_section_team.jsp";

    //MESSAGES specific to this application
    protected static final String MSG_PAGE_UPDATED          = "Page updated successfully.";
    protected static final String MSG_PAGE_DELETED          = "Page deleted successfully.";
    protected static final String MSG_PARENT_IS_SAME        = "ERROR: A page cannot be its own parent.";
    protected static final String MSG_DELETE_WITH_CHILDREN  = "Pages with children can't be deleted. Please delete the child pages first.";
    protected static final String MSG_DELETE_ROOT_PAGE      = "The root page cannot be deleted.";
    protected static final String MSG_NO_SECTION_SELECTED   = "You must select a section to edit.";
    protected static final String MSG_SECTION_UPDATED       = "Section updated successfully.";
    protected static final String MSG_TEAM_UPDATED          = "Team updated successfully.";
    protected static final String MSG_CONTENT_EXPIRED       = 
	"The content you have requested has expired. Please login to access expired content.";

    //QUERIES
    public static final String QRY_PAGES = 
	" SELECT page_id, LPAD(' ', (LEVEL-1)*2, '-') || name " +
        " FROM nav_page " +
        " START WITH page_id = 0 " +
        " CONNECT BY parent_page_id = PRIOR page_id " ;

    public static final String QRY_SECTIONS = 
	" SELECT section_id, section_name " +
        " FROM nav_section " + 
	" WHERE section_id >= 0 " +
        " ORDER BY UPPER(section_name) ASC ";

    public static final String QRY_TEMPLATES = 
	" SELECT template_id, template_name " + 
	" FROM nav_template " + 
	" ORDER BY sort_order ASC, UPPER(template_name) ASC ";

    public static final String QRY_TEMPLATE_ICONS = 
	" SELECT template_id, icon_path " + 
	" FROM nav_template " + 
	" ORDER BY sort_order ASC ";

    public static final String QRY_USER_GROUPS = 
	" SELECT group_id, COALESCE(display_name, group_name) " + 
	" FROM group_data " + 
	" ORDER BY sort_order ASC, UPPER(display_name) ASC, UPPER(group_name) ASC ";

    //The class of the User object
    protected Class USER_CLASS = com.codemagi.login.model.User.class;


    /**                                                                                                                    
     * HashMap of Vanity URLs for filter                                                                  
     */
    protected static HashMap vanityUrls = new HashMap(128);


    /**
     * Initialize the servlet
     */
    public void init(ServletConfig config) 
	throws ServletException {

	super.init(config);

	application.setAttribute("pageAdmin", this);

	lists = (Lists)application.getAttribute("lists");

        //load the list of vanity URLs into a hash for speed
        Database db = null;                                         

        try {
            //begin transaction
            db = getJDODatabase();
            db.begin();                              

            OQLQuery oql = db.getOQLQuery(" SELECT v FROM com.codemagi.servlets.content.model.VanityUrl v ");       
            QueryResults results = oql.execute(Database.ReadOnly);
            while (results.hasMore()) {
                VanityUrl v = (VanityUrl)results.next();
                log.debug("Loading vanity URL - PATH: " + v.getVanityUrl() + " PAGE: " + v.getPageId());                
                vanityUrls.put(v.getVanityUrl(), v.getPageId());
            }
            log.debug("vanityUrls: " + vanityUrls);

        } catch (PersistenceException pe) {
	    handlePersistenceException(db, pe, "Unable to load vanity URLs hash from database");

        } finally {
            closeJDODatabase(db);

        }

        //create validators
        IValidator integerValidator = new IntegerValidator();
        IValidator booleanValidator = new BooleanValidator();
        IValidator dateValidator = new DateValidatorUS();
        IValidator stripHtmlValidator = new StripHtmlValidator();
        IValidator notNullValidator = new NotNullValidator();
        IValidator stringLength256Validator = new StringLengthValidator(256);
        IValidator stringLength512Validator = new StringLengthValidator(512);
        IValidator stringLength1024Validator = new StringLengthValidator(1024);
        IValidator[] int_notNull_validators =
            { integerValidator, notNullValidator };
        IValidator[] strip_256_validators =
            { stripHtmlValidator, stringLength256Validator };
        IValidator[] strip_512_validators =
            { stripHtmlValidator, stringLength512Validator };
        IValidator[] strip_1024_validators =
            { stripHtmlValidator, stringLength1024Validator };
        IValidator[] strip_notNull_256_validators =
            { stripHtmlValidator, notNullValidator, stringLength256Validator };

        //add input validators
        addValidator("page", "returnList", stripHtmlValidator);
        addValidator("page", "id", integerValidator);
        addValidator("page", "parentId", integerValidator);
        addValidator("page", "name", strip_notNull_256_validators);
        addValidator("page", "title", strip_256_validators);
        addValidator("page", "keywords", stripHtmlValidator);  //size?
        addValidator("page", "link", strip_512_validators);
        addValidator("page", "sectionId", int_notNull_validators);
        addValidator("page", "templateId", integerValidator);
        addValidator("page", "groupId", integerValidator);
        addValidator("page", "sortOrder", integerValidator);
        addValidator("page", "loginRequired", booleanValidator);
        addValidator("page", "restrictToSection", booleanValidator);
        addValidator("page", "display", booleanValidator);
        addValidator("page", "displayHome", booleanValidator);
	//        addValidator("page", "content", stripHtmlValidator);  //size?
        addValidator("page", "expireDate", dateValidator);

        addValidator("deletePage", "id", int_notNull_validators);

        addValidator("section", "id", integerValidator);
        addValidator("section", "name", strip_notNull_256_validators);

        addValidator("setTeam", "sectionId", integerValidator);
        addValidator("setTeam", "assigned", new SetOfIntegersValidator()); 
        addValidator("setTeam", "unassigned", new SetOfIntegersValidator()); 

	//set the navigation commands this servlet will respond to 
	typeClasses.put("page", com.codemagi.servlets.content.model.DisplayNavPage.class);
        typeViews.put("page", VIEW_MAIN);
        typeNames.put("page", "navPage");

	typeClasses.put("section", com.codemagi.servlets.content.model.Section.class);
        typeViews.put("section", VIEW_SET_TEAM);
        typeNames.put("section", "section");

    }


    /**                                 
     * Initialize the filter
     */
    public void init(FilterConfig config)
        throws ServletException {

        log.debug("PageController FILTER INIT");
    }


    /**
     * Handles the incoming requests.
     * <p>
     * This implementation ensures authenticated session
     * existence, retrieves the <code>acton</code>
     * and <code>todo</code> parameters, and dispatches all
     * valid actions to the target dispatchers.
     * <p>
     * The flow of the process is described in the related
     * documentation.
     * <p>
     * Application related errors/exceptions are handled
     * by forwarding the request to an error page, or the actual page
     * in case of an inlined error.
     *
     * @param req a reference to the actual <code>HttpServletRequest</code>
     *        instance.
     * @param res a reference to the actual <code>HttpServletResponse</code>
     *        instance.
     *
     * @throws ServletException if servlet related operations fail.
     * @throws IOException if i/o operations fail.
     */
    public void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

        //set to true to view request info
	ServletUtils.logRequestParameters(request);

        String nextPage = VIEW_MAIN;

        //get the command or acton passed in from the URL                              
        String command      = request.getPathInfo();
	String action       = request.getParameter("acton");

	log.debug("COMMAND: " + command);
	log.debug("ACTON:   " + action);

	try {
	    //dispatch actions based on request
	    if ( !Utils.isEmpty(command) ) {
		nextPage = dispatchCommand(request, response);

	    } else if ("page".equals(action)) {
                nextPage = dispatchPage(request, response);

            } else if ("deletePage".equals(action)) {
                nextPage = dispatchDeletePage(request, response);

            } else if ("setTeam".equals(action)) {
		nextPage = dispatchSetTeam(request, response);

	    } else if ("section".equals(action)) {
		nextPage = dispatchSection(request, response);

	    }

        } catch (AccessException ae) {
	    nextPage = handleAccessException(request, response, ae);

        } catch (LoginException le) {
	    nextPage = handleLoginException(request, response, le);

        }

        //forward on
        log.debug("PageController FORWARDING TO: " + nextPage);
        if (!VIEW_NONE.equals(nextPage)) {
	    RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(nextPage);
	    dispatcher.forward(request, response);
	}

        return;
    }


    //FILTER METHODS ----------------------------------------------------------

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws ServletException, IOException {

	//        String requestUrl = ((HttpServletRequest)request).getServletPath();
        String requestUrl = ((HttpServletRequest)request).getRequestURI();

	//if the request URL has a trailing slash, remove it
	if (requestUrl != null && requestUrl.endsWith("/")) {
	    requestUrl = requestUrl.substring(0, requestUrl.length()-1);
	}	

        //determine if there is a page ID for this vanity URL
        Integer pageId = (Integer)vanityUrls.get(requestUrl);

        //if there is a page ID, forward to that page
        if ( pageId != null ) {
	    log.debug("VANITY URL: " + requestUrl + " PAGE ID:     " + pageId );	    

	    //load the page in question
	    Database db  = null;
	    NavPageContent page = null;

	    try {
		db = getJDODatabase();
                db.begin();

		page = (NavPageContent)db.load(NavPageContent.class, pageId, Database.ReadOnly);

	    } catch (PersistenceException pe) {
		handlePersistenceException(db, pe, "Error loading nav page for vanity URL");

	    } finally {
		closeJDODatabase(db);

	    }		

            // Get the request dispatcher -- request will be dispatched to this URL.
            RequestDispatcher rd;
	    if (page == null) {
		log.debug("doFilter: page is null: forwarding to: /page/" + pageId);
		rd = request.getRequestDispatcher("/page/" + pageId);

	    } else if (page.getLink() != null && page.getLink().startsWith("http")) {
		log.debug("doFilter: external link, redirecting to: " + page.getLink());
		((HttpServletResponse)response).sendRedirect(page.getLink());
		return;

	    } else if (page.getLink() != null && !page.getLink().startsWith("/page/")) {
		log.debug("doFilter: module link, redirecting to: " + page.getLink());
                ((HttpServletResponse)response).sendRedirect(page.getLink());
                return;

	    } else {
		log.debug("doFilter: forwarding to: " + page.getLink());
		rd = request.getRequestDispatcher(page.getLink());

	    }

            // Forward to requested URL
            rd.forward(request, response);

        } else {
            chain.doFilter(request, response);
        }

        return;
    }


    //DISPATCHERS -------------------------------------------------------------
    /* NOTE: DO NOT override dispatchCommand as this servlet extends NavigationController */

    /**
     * Overridden to implement NavPage-specific forwarding. 
     */ 
    protected String getNextPage(String type, String ext, Object obj, HttpServletRequest request, HttpServletResponse response) {

	log.debug("getNextPage() type: " + type + " ext: " + ext + " obj: " + obj);

	String nextPage = VIEW_MAIN;

	// determine where to go next    
	if ( !Utils.isEmpty(ext) ) {
	    
	    if ( ext.startsWith("/xml") ) {
		//return XML
		nextPage = returnXml(obj, ext, response);
		
	    } else {
		nextPage = ext;
	    }
		    
	} else if (obj instanceof NavPageContent) {
	    NavPageContent content = (NavPageContent)obj;
	    log.debug("content: " + content);

	    if ( !Utils.isEmpty(content.getContent()) || content.getLink().startsWith("/page") ) {
		//forward to either default template or page-specific template
		nextPage = Utils.isEmpty(content.getTemplatePath()) ? TEMPLATE : content.getTemplatePath();
		
	    } else {
		//an external link
		nextPage = content.getLink();
	    
	    }

	} else {
	    String typeView = (String)typeViews.get(type);
	    if (!Utils.isEmpty(typeView)) nextPage = typeView;
	}

	return nextPage;
    }


    /**
     * Dispatches <code>page</code> actons
     */
    protected String dispatchPage(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchPage *****");

        String nextPage   = VIEW_EDIT_PAGE;

        //must be logged in to edit user info
        HttpSession session = request.getSession();

        Database db    = null;

        try {
	    String returnList = getString("returnList", request);
	    request.setAttribute("returnList", returnList);

            //check for login
            IUser user = checkLogin(request);

            //persist
            db = getJDODatabase();
            db.begin();

            //the ID of the Page to edit
            Integer id       = getInteger("id", request);

            //the ID of the parent to add this to
            Integer parentId = getInteger("parentId", request);

            NavPageContent page   = null;
            NavPageContent parent = null;

            if (id == null) {
                //we are adding a new page
                page = new NavPageContent();

                //if this page has a parent, load it
                if ( parentId != null ) {
                    parent = (NavPageContent)db.load(NavPageContent.class, parentId);

                    //check that user can add sub page
                    if (!parent.hasUser(user, NavPageContent.ACCESS_EDIT)) throw new AccessException("");

                    //by default, make page a member of same section as parent
                    page.setSection( ((NavPageContent)parent).getSection());

                }

            } else {
                //existing page
                page   = (NavPageContent)db.load(NavPageContent.class, id);
                parent = (NavPageContent)page.getParent();

                //check that user can edit this page
                if (!page.hasUser(user, NavPageContent.ACCESS_EDIT)) throw new AccessException("");
            }

            //set the page back into request in case we get any Exceptions
            request.setAttribute("navPage", page);
            request.setAttribute("parent", parent);
            log.debug("USING PAGE: " + page);
            log.debug("USING PARENT: " + parent);

            //if the user actually pressed submit, perform the updates
            //otherwise just forward to the editing page
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit) ) {
                if (id != null) {
                    //load the vanity URLs for this nav page and attach them to the request object                                
                    OQLQuery oql = db.getOQLQuery(" SELECT v FROM com.codemagi.servlets.content.model.VanityUrl v " +
                                                  " WHERE v.pageId = $1 ");
                    oql.bind(id);

                    QueryResults results = oql.execute();
                    ArrayList vUrls      = new ArrayList();
                    while (results.hasMore()) {
                        VanityUrl v = (VanityUrl)results.next();

                        vUrls.add(v);
                    }

                    page.setVanityUrls(vUrls);
                    request.setAttribute("navPage", page);
                }

                nextPage = VIEW_EDIT_PAGE;

            } else {

                //get data from form
                String name               = getString("name", request);
                String title              = getString("title", request);
                String keywords           = getString("keywords", request);
                String link               = getString("link", request);
                Integer sectionId         = getInteger("sectionId", request);
                Integer templateId        = getInteger("templateId", request);
		Integer groupId           = getInteger("groupId", request);
                Integer sortOrder         = getInteger("sortOrder", request);
                Boolean loginRequired     = getBoolean("loginRequired", request);
                Boolean restrictToSection = getBoolean("restrictToSection", request);
                Boolean display           = getBoolean("display", request);
                Boolean displayHome       = getBoolean("displayHome", request);
                String content            = getString("content", request);
                Date expireDate           = getDate("expireDate", request);

                Enumeration vUrls         = FormUtils.getParametersByName(request, "vanityUrl:");

                log.debug("CONTENT SUBMITTED: " + content);

		//perform CSRF token test
		checkToken(request);

		//did we get any validation errors?
		checkValidation(request);

                //a page cannot be its own parent
                if ( parentId != null && (!ROOT_PAGE_ID.equals(id) && parentId.equals(id)) ) {
                    request.setAttribute("error_parentId_error", "true");
                    throw new AppException(MSG_PARENT_IS_SAME);
                }

                //if the parent has changed, remove it from the old parent
                if ( parentId != null && !parentId.equals( page.getParentId() ) ) {
                    parent.removeChild(page);

                    parent = (NavPageContent)db.load(NavPageContent.class, parentId);
                }

                //set the parent page, if this is not the root page
		//                if (!ROOT_PAGE_ID.equals(id)) {
		if (!page.isRootPage()) {
                    parent.addChild(page);
                }

                log.debug("PARENT ID: "+ page.getParentId());

                //load dependent Objects
                Section section = (Section)db.load(Section.class, sectionId);

                //load the template for this page if a templateId was submitted
                NavTemplate template = null;
                if (templateId != null) {
                    template = (NavTemplate)db.load(NavTemplate.class, templateId);
                }

                page.setName( name );
                page.setTitle( title );
                page.setKeywords( keywords );
                page.setLink( link );
                page.setSection( section );
                page.setSortOrder( sortOrder );
                page.setLoginRequired( loginRequired );
                page.setRestrictToSection( restrictToSection );
                page.setDisplay( display );
                page.setDisplayHome( displayHome );
                page.setContent(content);
                page.setContentDate(new Date());
                page.setExpireDate(expireDate);
                page.setTemplate(template);

		boolean newPage = page.getId() == null;

                //set node fields
                setNodeFields(page, request, db);  //saves new page in DB if needed

		//add taxonomy terms
		com.codemagi.servlets.TaxonomyController.setTerms(page, request, db);

                //create Object in DB if necessary
                if (newPage) {
                    log.debug("NEW PAGE ID: "+ page.getId());
                    log.debug("NEW PAGE PARENT ID: "+ page.getParentId());

                    //update the parent
                    log.debug("Adding child to: " + parent);
                    parent.addChild(page);
                    setNodeFields(parent, request, db);
                }

                //resolve the link
                if (Utils.isEmpty(link)) link = "/page/" + page.getId();
                page.setLink( link );

                //deal with vanity URLs
                while (vUrls.hasMoreElements()) {

                    Integer vanityUrlId = convertInteger( (String)vUrls.nextElement(), true );
                    String vanityUrl    = request.getParameter("vanityUrl:" + Utils.noNulls(vanityUrlId) );

		    //if the vanity URL does not start with a slash, add one
		    if (!Utils.isEmpty(vanityUrl) && !vanityUrl.startsWith("/")) vanityUrl = "/" + vanityUrl;

                    //if the vanity URL has a trailing slash, remove it
                    if (!Utils.isEmpty(vanityUrl) && vanityUrl.endsWith("/")) {
                        vanityUrl = vanityUrl.substring(0, vanityUrl.length()-1);
                        log.debug("VANITY URL, Trailing / removed: " + vanityUrl );
                    }

                    OQLQuery oql = db.getOQLQuery(" SELECT v FROM com.codemagi.servlets.content.model.VanityUrl v " +
                                                  " WHERE v.id = $1 ");
                    oql.bind(vanityUrlId);

                    log.debug("PRE:  vanityUrls: " + vanityUrls);

                    VanityUrl v = null;

                    QueryResults results = oql.execute();
                    if (results.hasMore()) {

                        v = (VanityUrl)results.next();

                        if (Utils.isEmpty(vanityUrl)) {
                            //no url specified, remove db and hash entries
                            db.remove(v);

                            vanityUrls.remove(v.getVanityUrl());      //remove old entry from hash                                

                        } else {
                            //update the existing vanity Url
                            vanityUrls.remove(v.getVanityUrl());      //remove old entry from hash                                
                            v.setVanityUrl(vanityUrl);                //update bean and DB                                        
                            vanityUrls.put(vanityUrl, id);        //put new entry in hash                                         

                        }

                    } else if (!Utils.isEmpty(vanityUrl)) {

                        //create a new vanity URL
                        v = new VanityUrl();
                        v.setVanityUrl(vanityUrl);
                        v.setPageId(page.getId());
                        db.create(v);

                        //add to the hash
                        vanityUrls.put(vanityUrl, page.getId());
                    }

                    if (v != null) page.addVanityUrl(v);

                    log.debug("POST: vanityUrls: " + vanityUrls);
                }

                db.commit();

                log.debug("CONTENT IN BEAN AFTER SUBMIT: " + content);

                //success!
                request.setAttribute("error_message", MSG_PAGE_UPDATED);

                //update lists
                Lists lists = (Lists)application.getAttribute("lists");
                if (lists != null) lists.update(QRY_PAGES);

                //expire the performance cache
                CacheManager cm = db.getCacheManager();
                Class[] type    = { TabPage.class, NavPage.class, NavPageContent.class, DisplayNavPage.class } ;
                cm.expireCache(type);

                //next page should be the page itself, unless a different return page is specified                                
                if (!Utils.isEmpty(returnList)) {
                    nextPage = VIEW_LIST_PAGES;
                    //tell the template to scroll to the last item viewed                                                         
                    request.setAttribute("onLoad", "gotoAnchor('id" + page.getParentPageId() + "');");

                } else if ( !link.startsWith("/") ) {
                    //instead of forwarding to an external page, forward to the parent                                      
                    nextPage = ((NavPageContent)parent).getLink();

                } else {
                    nextPage = page.getLink();
                }

            }

            request.setAttribute("navPage", page);


        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

        } finally {
            closeJDODatabase(db);
        }


        return nextPage;
    }


    /**
     * Dispatches <code>deletePage</code> actions.
     */
    protected String dispatchDeletePage(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchDeletePage *****");

        String nextPage = VIEW_DELETE_PAGE;

        Database db = null;

        try {
            //check for login
            IUser user = checkLogin(request);

            //get request params
            Integer pageId = getInteger("id", request);
            log.debug("Page ID to delete: " + pageId);

	    //make sure we are not deleting the root page
	    if (ROOT_PAGE_ID.equals(pageId)) throw new AppException(MSG_DELETE_ROOT_PAGE);

            //persist
            db = getJDODatabase();
            db.begin();

            //load the page to delete
            NavPageContent page = (NavPageContent)db.load(NavPageContent.class, pageId);
            log.debug("loaded page to delete: " + page);

            //set page into request in case we get any exceptions
            request.setAttribute("navPage", page);

            //make sur euser has permission to delete the page
            if (!page.hasUser(user, NavPageContent.ACCESS_DELETE)) throw new AccessException("");

            //if the user actually pressed submit, delete the page
            //otherwise, send them to confirmation page
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit) ) {
                nextPage = VIEW_DELETE_PAGE;

            } else {

		//perform CSRF token test
		checkToken(request);

		//did we get any validation errors?
		checkValidation(request);

                //check that this page doesn't have sub-pages
                if (page.hasChildren() ) throw new AppException(MSG_DELETE_WITH_CHILDREN);

                //delete the page from the DB
                db.remove(page);

                db.commit();

                //success!
                request.setAttribute("error_message", MSG_PAGE_DELETED);

                //update lists
                Lists lists = (Lists)application.getAttribute("lists");
                if (lists != null) lists.update(QRY_PAGES);

                //expire the page from the cache
                CacheManager cm = db.getCacheManager();
                Class[] type    = { NavPageContent.class, TabPage.class, DisplayNavPage.class } ;
                cm.expireCache(type);

                //next page should be the parent of the deleted page, unless otherwise specified                              
                NavPageContent parent = (NavPageContent)page.getParent();
                nextPage = getReturnPage(request, parent.getLink());
            }

        } catch (AppException sne) {
            log.debug("", sne);
	    String errorMessage = sne.getMessage();
            request.setAttribute("error_message", errorMessage);

            nextPage = MSG_DELETE_ROOT_PAGE.equals(errorMessage) ? VIEW_LIST_PAGES : VIEW_DELETE_PAGE;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_DELETE_PAGE;

        } finally {
            closeJDODatabase(db);
        }

        return nextPage;
    }


    /**
     * Dispatches <code>section</code> actons
     */
    protected String dispatchSection(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

        log.debug("***** dispatchSection *****");

        String nextPage   = VIEW_EDIT_SECTION;

        //must be logged in to edit user info
        HttpSession session = request.getSession();

        Database db = null;

        try {
            //check for login and access
	    IUser user = checkPermission(request, ACCESS_SUPERUSER);

            //persist
            db = getJDODatabase();
            db.begin();

            //load the Section to edit
            Integer sectionId = getInteger("id", request);

            Section section = null;
            if (sectionId == null) {
		//we are adding a new section
                section = new Section();

            } else {
                section = (Section)db.load(Section.class, sectionId);

            }

	    //set section into request in case we get any exceptions
            request.setAttribute("section", section);

            //if the user actually pressed submit, perform the updates
            //otherwise just forward to the editing page
            String submit = request.getParameter("isSubmit");
            if (Utils.isEmpty(submit) ) {
                nextPage = VIEW_EDIT_SECTION;

            } else {

                //get data from form
                String name = getString("name", request);

		//perform CSRF token test
		checkToken(request);

		//did we get any validation errors?
		checkValidation(request);

                section.setName( name );

                //create Object in DB if necessary
                if (section.getId() == null) {
                    db.create(section);
                }

                db.commit();

                //success! 
                request.setAttribute("error_message", MSG_SECTION_UPDATED);

                //update lists
		Lists lists = (Lists)application.getAttribute("lists");
                if (lists != null) lists.update(QRY_SECTIONS);

		//forward them to the page to set the section team
		nextPage = getReturnPage(request, VIEW_SET_TEAM);
		
            }

            request.setAttribute("section", section);

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

        } finally {
            closeJDODatabase(db);
	}


        return nextPage;
    }


    /**
     * Dispatches <code>setTeam</code> actions: Adds or removes users from a team.
     */
    public String dispatchSetTeam(HttpServletRequest request, HttpServletResponse response)
        throws LoginException, AccessException {

	log.debug("***** dispatchSetTeam *****");

        //next page if successful                                  
        String nextPage = VIEW_SET_TEAM;

        Database db = null;

        try {
            //check for permissions
            IUser user = checkLogin(request);

            //check that they have a section selected     
	    Integer sectionId = getInteger("sectionId", request);
            if (sectionId == null) throw new AppException(MSG_NO_SECTION_SELECTED);

	    //persist
	    db = getJDODatabase();
	    db.begin();
	    
	    //load the Section bean
	    Section section = (Section)db.load(Section.class, sectionId);
	    
	    //only users of the section may add more users
	    if (!section.hasUser(user) ) throw new AccessException("");

	    //set section into request in case we get any errors
	    request.setAttribute("section", section);
	    
            //if the user actually pressed submit, perform the updates
            //otherwise just forward to the editing page
            if ( Utils.isEmpty( request.getParameter("isSubmit") ) ) {
                nextPage = VIEW_SET_TEAM;

            } else {

		//perform CSRF token test
		checkToken(request);

		String addToTeam           = getString("addToTeam", request);
		String removeFromTeam      = getString("removeFromTeam", request);
		Set<Integer> removeUserIds = getSet("assigned", request);
		Set<Integer> addUserIds    = getSet("unassigned", request);

		//did we get any validation errors?
		checkValidation(request);

                //are we adding or removing?
                if ( !Utils.isEmpty(addToTeam) ) {
                    //we are adding people to the team from the unassigned list
                    log.debug("ADDING TEAM MEMBERS: " + addUserIds);

                    //set new team members into section
		    for (Integer userId : addUserIds) {
			User member = (User)db.load(USER_CLASS, userId);

			section.addUser(member);
		    }

                } else {
                    //we are removing existing members from the assigned list
                    log.debug("REMOVING TEAM MEMBERS: " + removeUserIds);

		    for (Integer userId : removeUserIds) {
			User member = (User)db.load(USER_CLASS, userId);

			section.removeUser(member);
		    }
			
                }

                //commit DB changes
                db.commit();

		//success!
                request.setAttribute("error_message", MSG_TEAM_UPDATED);
            }

        } catch (AppException se) {
            request.setAttribute("error_message", se.getMessage());

            nextPage = VIEW_SET_TEAM;

        } catch (PersistenceException e) {
	    handlePersistenceException(request, db, e);

            nextPage = VIEW_SET_TEAM;

        } finally {
            closeJDODatabase(db);
        }

        return nextPage;
    }




    //UTILITY METHODS ---------------------------------------------------------

    public Integer getPageSectionId(Integer pageId) {

        log.debug("PageController.getPageSectionId(" + pageId + ")");

        Database db = null;

        try {
            db = getJDODatabase();
            db.begin();

            NavPageContent page = (NavPageContent)db.load(NavPageContent.class, pageId, Database.ReadOnly);

            return page.getSectionId();

        } catch (PersistenceException e) {
            handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);
        }

        return null;
    }


    public TabPage getTabPage(Integer pageId) {

        Database db = null;

        TabPage output = new TabPage();

        try {
            db = getJDODatabase();
            db.begin();

            output = (TabPage)db.load(TabPage.class, pageId, Database.ReadOnly);

            db.rollback();

        } catch (Exception e) {
            log.debug("", e);

        } finally {
            closeJDODatabase(db);

        }

        return output;
    }


    public TabPage getTabPage(String link) {

        Database db = null;

        TabPage output = new TabPage();

        try {
            db = getJDODatabase();
            db.begin();

            output = getTabPage(link, db);

            db.rollback();

        } catch (Exception e) {
            log.debug("", e);

        } finally {
            closeJDODatabase(db);

        }

        return output;
    }


    public static TabPage getTabPage(String link, Database db)
        throws PersistenceException {

        TabPage output = new TabPage();

        if (db == null || link == null)
	    return output;

        OQLQuery oql = db.getOQLQuery(" SELECT p FROM com.codemagi.servlets.content.model.TabPage p " +
                                      " WHERE p.link = $1 ");
        oql.bind(link);

        QueryResults results = oql.execute();
        if (results.hasMore()) {
            output = (TabPage)results.next();
        }

        return output;
    }


    public NavPageContent getNavPageContent(Integer pageId) {

        Database db = null;

        NavPageContent output = new NavPageContent();

        try {
            db = getJDODatabase();
            db.begin();

            output = (NavPageContent)db.load(NavPageContent.class, pageId, Database.ReadOnly);

            db.rollback();

        } catch (PersistenceException e) {
            handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);

        }

        return output;
    }


    public NavPageContent getNavPageContent(String link) {

        Database db = null;

        NavPageContent output = new NavPageContent();

        try {
            db = getJDODatabase();
            db.begin();

            output = getNavPageContent(link, db);

            db.rollback();

        } catch (PersistenceException e) {
            handlePersistenceException(db, e);

        } finally {
            closeJDODatabase(db);

        }

        return output;
    }


    public static NavPageContent getNavPageContent(String link, Database db)
        throws PersistenceException {

        NavPageContent output = new NavPageContent();

        if (db == null || link == null)
	    return output;

        OQLQuery oql = db.getOQLQuery(" SELECT p FROM com.codemagi.servlets.content.model.NavPageContent p " +
                                      " WHERE p.link = $1 ");
        oql.bind(link);

        QueryResults results = oql.execute(Database.ReadOnly);
        if (results.hasMore()) {
            output = (NavPageContent)results.next();
        }

        return output;
    }


    /**
     * Recursive method to generate an OrderedTable of the page hierarchy tree.
     */
    public OrderedTable getPageHierarchy() {

	OrderedTable output = new OrderedTable();
	int level           = 0;

	//get the root nav page
	NavPageContent page = getNavPageContent(ROOT_PAGE_ID);

	output.put(page.getId()+"", page.getName());

	//add the children
	for (NavPageContent child : page.getChildren(new NavPageContent())) {
	    getPageHierarchy( output, child, level+1 );
	}

	return output;
    }


    /**
     * Recursive helper method for getPageHierarchy()
     */
    private void getPageHierarchy(OrderedTable output, NavPageContent page, int level) {

	//sanity check
	if (page == null) return;

	output.put(page.getId()+"", StringUtils.pad(" ", level+1, '-') + page.getName() );

	//add the subordinates
	for (NavPageContent child : page.getChildren(new NavPageContent())) {
	    getPageHierarchy( output, child, level+1 );
	}

    }

    /*
    protected String returnXml(Object obj, String ext, HttpServletResponse response) {

	log.debug("returnXml( " + obj + ", " + ext + ", response )");

	String command = ext.substring(1);  //trim the leading slash
	String[] split = command.split("/", 2);
	log.debug("SPLIT: " + CollectionUtils.commaDelimit(split) );

	if (split.length < 1) return ext;
	log.debug("1");

	if ( !"xml".equals(split[0]) ) return ext;
	log.debug("2");

	//writer to output response
	PrintWriter writer = null;

	try {

	    Stopwatch timer = new Stopwatch();
	    timer.start();
	    
	    String xml    = marshal(obj, mapping);
	    String output = "";

	    timer.lap();
	    log.debug("XML Marshalling complete in: " + timer.getLapTime());

	    //prevent browser caching this XML
	    response.setHeader("Cache-Control","no-store");  //HTTP 1.1   
	    response.setHeader("Pragma","no-cache"); //HTTP 1.0

	    //prevents caching at the proxy server     
	    response.setDateHeader("Expires", -1);  

	    if ( split.length > 1 && !Utils.isEmpty(split[1]) && split[1].contains(".xsl") ) {
		//stylesheet specified
		log.debug("stylesheet: " + split[1]);
		
		output = transformXml(xml, "/" + split[1]);
		response.setContentType("text/plain");

		timer.lap();
		log.debug("XSL Transformation complete in: " + timer.getLapTime());
		
	    } else {
		//just use the raw XML
		log.debug("returning raw XML");

		output = xml;
		
	    }

	    //send output to the browser
	    log.debug("OUTPUT: " + output);
	    writer = response.getWriter();
	    writer.write(output);
	    writer.flush();

	    timer.stop();
	    log.debug("returnXML complete in: " + timer.getElapsedTime());

	    return VIEW_NONE;

	} catch (IOException ioe) {
	    log.debug("", ioe);

	} catch (MappingException me) {
	    log.debug("", me);
            
	} catch (XMLException xe) {
	    log.debug("", xe);

	} finally {
	    closeStream(writer);

	}

	return ext;
    }
    */
}
